A continuación se expone el código y comentarios resultados de la realización de la práctica 1.

0. Imports

Importar librerias

library(tidyverse)
library(tidyr)
library(dplyr)
library(knitr)
library(ggplot2)
library(stringr)
library(reshape)
library(minerva)
library(heatmaply)
library(kableExtra)
library(factoextra)
library(ggbiplot)
library(randomForest)
library(ROCR)
library(ggcorrplot)
library(ModelMetrics)

Importar datos

Aunque se nos entrega el fichero de train entero y podriamos obtener la variable respuesta, se trataran cómo si de un caso real se tratara, y todo el EDA y el training se usara solo el p1_train. El p1_test se usara únicamente para validar los resultados al final de todo de la practica.

train <- read.csv("./data/train.csv", head = TRUE, stringsAsFactors=TRUE)

p1_train <- read.csv("./data/p1_train.csv", head = TRUE, sep=";", stringsAsFactors=TRUE)

p1_test <- read.csv("./data/p1_test.csv", sep=";", head = TRUE, stringsAsFactors=TRUE)
p1_test_fin <- read.csv("./data/p1_test.csv", sep=";", head = TRUE, stringsAsFactors=TRUE)

El df p1_test_fin se crea para mantener los datos base sin transformaciones para la generación final del resultado.

1. Preprocesado de los datos

En este apartado se va hacer el análisi exploratorio de los datos.

head(p1_train)

Eliminamos la variable id del dataset ya que es el indice, y no aporta información.

p1_train <- p1_train %>% select(-id)

Summary de los datos

summary(p1_train)
      year           hour           season         holiday        workingday        weather          temp           atemp      
 Min.   :2011   Min.   : 0.00   Min.   :1.000   Min.   :0.000   Min.   :0.0000   Min.   :1.00   Min.   : 0.82   Min.   : 0.76  
 1st Qu.:2011   1st Qu.: 6.00   1st Qu.:2.000   1st Qu.:0.000   1st Qu.:0.0000   1st Qu.:1.00   1st Qu.:13.94   1st Qu.:16.66  
 Median :2011   Median :12.00   Median :3.000   Median :0.000   Median :1.0000   Median :1.00   Median :20.50   Median :24.24  
 Mean   :2011   Mean   :11.57   Mean   :2.506   Mean   :0.029   Mean   :0.6773   Mean   :1.41   Mean   :20.27   Mean   :23.70  
 3rd Qu.:2012   3rd Qu.:18.00   3rd Qu.:4.000   3rd Qu.:0.000   3rd Qu.:1.0000   3rd Qu.:2.00   3rd Qu.:26.24   3rd Qu.:31.06  
 Max.   :2012   Max.   :23.00   Max.   :4.000   Max.   :1.000   Max.   :1.0000   Max.   :3.00   Max.   :41.00   Max.   :45.45  
    humidity        windspeed          count      
 Min.   :  0.00   Min.   : 0.000   Min.   :  1.0  
 1st Qu.: 46.00   1st Qu.: 7.002   1st Qu.: 41.0  
 Median : 62.00   Median :12.998   Median :145.0  
 Mean   : 61.77   Mean   :12.802   Mean   :191.4  
 3rd Qu.: 77.00   3rd Qu.:16.998   3rd Qu.:283.0  
 Max.   :100.00   Max.   :56.997   Max.   :977.0  

1.0 Nan handling

Se puede apreciar que no encontramos ningún nan en las columnas del dataset, haciendo que no tengamos que tratar con ellos.

p1_train %>% group_by() %>% summarise_all(funs(sum(is.na(.))))

1.1 Duplicates handling

Se eliminan las filas duplicadas del dataset.

p1_train <- p1_train %>%
                distinct()
p1_train

1.2. Separar datos entre numericos y categoricos

Para las variables que son categoricas, aunque su representación sea numerica debemos tratarlas cómo categoricas a nivel dato para obtener el resultado deseado.

Las variables categoricas del dataset segun el enunciado son: workingday, holiday, weather y season.

p1_train <- p1_train %>% mutate(workingday = as.factor(workingday),
                                   holiday = as.factor(holiday),
                                   weather = as.factor(weather),
                                   season = as.factor(season))

2. Exploratory Data Analysis

Eliminamos la variable target del dataset para hacer un estudio de las columnas predictivas. Posteriormente se hará el estudio de la variable target independientemente.

predictors <- p1_train %>% select(-count)

2.0. Hacer boxplots de los campos y mirar outliers en los datos

Estudio de las variables predictivas numericas.

meltPred <- predictors %>% select(where(is.numeric)) %>% melt()
Using  as id variables
meltPred %>%
 ggplot(aes(factor(variable), value)) +
   geom_violin(width=1, color = "gray", alpha = 0.2, fill = 'green' ) +
    geom_boxplot(width=0.3, color="black", fill='blue', alpha=0.3, outlier.colour="red", outlier.shape=8,
             outlier.size=1, notch=TRUE) + facet_wrap(~variable, scale="free")
Notch went outside hinges
ℹ Do you want `notch = FALSE`?

Estos gráficos representan un boxplot y un violinplot sobre todas las variables numericas del dataset. Podemos apreciar:

  • La columna year contiene únicamente dos valores 2011 y 2012.

  • La columna hour se ditribuye entre el 0 y el 24, con una media situada en los 12. No encontramos outliers.

  • La columna temp y la columna atemp siguen distribuciones muy similares, la media esta alrededor del 25. No encontramos outliers.

  • La columna humidity se distribuye entre el 0 y el 100 con una media alrededor de los 60. No encontramos otuliers.

  • La columna windspeed se distribuye entre el 0 y el 60 con una media alrededor de los 15. Encontramos numersos outliers cuando el valores es mayor de 30.

2.1 Matriz de correlaciones de pearson de las variables numericas

corrp <- p1_train %>% select(where(is.numeric)) %>%  cor(method = 'pearson')
corrp %>% ggcorrplot(hc.order = TRUE, type = 'lower', lab=TRUE,
                      outline.col = "white",
                       ggtheme = ggplot2::theme_gray,
                       colors = c("#6D9EC1", "white", "#E46726")
                      )

Se puede apreciar cómo la variable atemp y temp estan muy correlacionadas, de ambas, seleccionaremos solamente la columna temp y eliminaremos la columna atemp para una mejora en la prediccion. Con la variable target count, estan directamente relacionadas las columnas temp, year, hour y windspeed y la columna humidity esta indirectamente realcionada.

Eliminamos la columna atemp del dataset.

p1_train <- p1_train %>% select(-atemp)
p1_test <- p1_test %>% select(-atemp)
predictors <- predictors %>% select(-atemp)

2.2 matriz de correlaciones lineales y no lineales con la variable target ‘count’

Calculamos la correlación no lineal MIC de las columnas con la variable target.

numpred <- predictors %>% select(where(is.numeric)) 
corrs <- data.frame(MIC = mine(numpred, y = p1_train$count, alpha = 0.7)) %>% select(MIC.Y)
corrs$Pearson <- cor(numpred, y = p1_train$count, method = 'pearson')
rownames(corrs) <- rownames(corrs$Pearson)
corrs

Podremos apreciar que las correlaciones no linealas calculadas son (en proporcion) similares a las correlaciones de Pearson, entendiendo entonces que no hay correlaciones obviadas en el grafico de correlaciones.

cbind(corrs$MIC.Y, corrs$MIC.Y ) %>% heatmaply_cor(xlab = "",
              ylab = "Columnas", main = "Correlacion MIC de las columnas numericas con la variable target 'count'", cellnote = cbind(corrs$MIC.Y, corrs$MIC.Y ), k_col = 1, k_row = 1)
NA

El gràfico superior tiene 2 columnas pero se debe considerar una sola (estan repetidas), ha sido la unica manera que hemos encontrado de hacer el gráfico del MIC.

2.3 PCA

Realizamos un PCA sobre los datos para extraer los componentes principales y estudiarlos.

pca <- predictors %>% select(where(is.numeric)) %>% sample_n(200) %>%
  prcomp(scale. = TRUE, center = TRUE) #Se debe hacer un sample para posteriormente poder apreciar graficamente la distribución PCA.
summary(pca)
Importance of components:
                         PC1    PC2    PC3    PC4    PC5
Standard deviation     1.257 1.1315 0.9295 0.8425 0.7523
Proportion of Variance 0.316 0.2560 0.1728 0.1419 0.1132
Cumulative Proportion  0.316 0.5721 0.7449 0.8868 1.0000
pca %>% 
  ggbiplot::ggbiplot(scale = 1)

Gràfico que representa la ponderación de las variables utilizadas para el PCA.

3. Analisis exploratorio de la variable target ‘count’

La variable target ‘count’ es una variable numerica que representa el numero de bicicletas alquiladas en las grandes ciudades. La variable tiene una media de 190 y un std de 182, un valor bastante elevado que indica mucha variación en la variable.

corrs$Pearson
                [,1]
year       0.2623084
hour       0.4037404
temp       0.3971578
humidity  -0.3245222
windspeed  0.1078587

Correlaciones de pearson de las columnas predictivas con la variable ‘count’.

mean(p1_train$count)
[1] 191.5154
sd(p1_train$count)
[1] 182.139

3.1 Distribución

El gráfico siguiente es un boxplot y un violin plot sobre la variable target ‘count’. Por el violin plot se aprecia que la mayoria de valores se encuentran cercanos al 0 pero por el boxplot se visualiza que los valores altos del dataset hacen que la media suba considerablemente. En el boxplot podemos apreciar también diversos outliers que sobresalen a partir del valor 550.

p1_train %>%
  ggplot( aes(x=count, y=count)) +
    geom_violin(width=1, color = "gray", alpha = 0.2, fill = 'green' ) +
    geom_boxplot(width=0.3, color="black", fill='blue', alpha=0.3, outlier.colour="red", outlier.shape=8,
             outlier.size=1, notch=TRUE) +
    theme(
      legend.position="none",
      plot.title = element_text(size=11)
    ) +
    ggtitle("Violin y box plot de los valores de la variable target ") +
    xlab("") + theme_classic() 

3.2 Histograma y densidad

Partiendo de la base que la variable target es continua, podemos apreciar que la frequencia de la misma sigue una distribución parecida a la exponencial, ya que cómo más se aleja del número 0 menos frequencia encontramos. Habrá que tener en cuenta entonces, que el modelo va a tender a predecir valores bajos en la mayoria de los casos.

p1_train %>%
ggplot( aes(x=count)) + 
 geom_histogram(aes(y=..density..), colour="black", fill="white", binwidth = 20)+
 geom_density(alpha=.2, fill="#FF6666") + theme_classic()

4. Prediccion

4.1 Normalización y preparación de los datos

Hacemos un escalado sobre los datos para normalizar.

p1_train <- p1_train %>% mutate(across(where(is.numeric), scale))

Separamos el dataset para train y test, para poder hacer validaciones sobre el modelo.

p <- 0.8
n <- nrow(p1_train)
set.seed(12345)
train.sel <- sample(c(FALSE, TRUE), n, rep = TRUE, prob=(c(1-p,p)))
train <- p1_train[train.sel,]
test <- p1_train[!train.sel,]

train

4.2 Linear model

Entrenamos un modelo lineal con los datos del train

mod_lm <- lm(count~., train) 

Calculamos el mse sobre el train

mean(mod_lm$residuals^2) #mse
[1] 20124.92

Calculamos el msle sobre el train

mean(log(mod_lm$residuals^2)) #mse
[1] 8.414306

Se puede apreciar que el mse y msle es muy grande, el modelo no funciona bien.

plot(p1_train$count)
abline(mod_lm)

Se puede ver cómo un modelo lineal es demasiado sencillo para entender la complejidad de los datos y senzillamente predice la media del resultado.

Podremos coger el error 20000 (que representa el mse entre la media y cada valor) para poder decidir en los modelos posteriores si estan aprendiendo.

Validación sobre el test

preds_test_lm <- predict(mod_lm, test)
predsLM <- data.frame(pred = preds_test_lm, gt = test$count)
predsLM

Se puede apreciar cómo era de esperar, que la performance del modelo sobre el dataset de validación es mala.

Calculamos el MSE sobre el test

mean((predsLM$pred-predsLM$gt)^2) #mse
[1] 19119.78

Calculamos RMSLE (error cuadratico medio logaritmico) sobre el test

rmsle(predsLM$gt,predsLM$pred)
[1] NaN

4.3 glm

mod_glm <- glm(count~., train, family = poisson())
mean(mod_glm$residuals^2)
[1] 0.7673455

Se ha intentado analizar la predicción pero no se encuentra sentido en la misma. El mse de 0.79 parece demasiado bajo para ser correcto y el predict sobre el train da valores count lejanos al ground truth.

4.4 Random forest

set.seed(1234) #definimos una random seed para obtener los mismos resultados consistentemente

Definimos el modelo. Durante la realización de la práctica se ha hecho un grid search y se definen los parametros que devuelven mejor resultado.

rf <- randomForest(count~., data = train, mtry = 10, importance = TRUE, ntree = 50 )
rf

Call:
 randomForest(formula = count ~ ., data = train, mtry = 10, importance = TRUE,      ntree = 50) 
               Type of random forest: regression
                     Number of trees: 50
No. of variables tried at each split: 9

          Mean of squared residuals: 2346.799
                    % Var explained: 93.01

Visualizamos que predictores son más importantes.

importance(rf)
              %IncMSE IncNodePurity
year        74.922249      18359755
hour       163.338072     123392475
season      17.796134       7446561
holiday      8.081658        720623
workingday  87.259677      15505385
weather     19.430596       3216781
temp        47.551007      25933109
humidity    27.728905       7634230
windspeed    6.629868       2741114
varImpPlot(rf)

Podemos apreciar cómo la variable ‘hour’ es con diferencia la que mejor ayuda a predecir el numero de bicicletas alquiladas.

Analisis del model performance (mse) sobre el conjunto de train

mseDF <- data.frame(pred = rf$predicted, gt = rf$y)
mseDF

Podemos ver las diferencias entre el count predecido y el ground truth.

mean((mseDF$pred-mseDF$gt)^2) #mse
[1] 2346.799

Calculamos RMSLE (error cuadratico medio logaritmico)

rmsle(mseDF$gt,mseDF$pred)
[1] 0.3805507

Obtenemos un mse y rmsle coherentes, el modelo funciona.

Validacion del modelo sobre el test

pred_test_rf <- predict(rf, test)
mseDFtest <- data.frame(pred = pred_test_rf, gt = test$count)
mseDFtest

Podemos ver las diferencias entre el count predecido y el ground truth.

mean((mseDFtest$pred-mseDFtest$gt)^2) #mse
[1] 2133.669

Calculamos RMSLE (error cuadratico medio logaritmico)

rmsle(mseDFtest$gt,mseDFtest$pred)
[1] 0.3766893

El resultado del mse y el rmsle sobre el dataset de test es muy pareciodo a los resultados de train, de esta manera podemos concluir que nuestro modelo no tiene overfitting y es capaz de generalizar y tener buen rendimiento predictivo sobre datos que no ha visto con anterioridad.

4.4.1 Random Forest 2

Volvemos a hacer random forest con variables más correlacionadas con la columna a predecir.

trainImp <- train %>% select('hour','year','workingday','season','humidity', 'count')

Entrenamos random forest

rf2 <- randomForest(count~., data = trainImp, mtry = 10, importance = TRUE, ntree = 50 )
rf2

Call:
 randomForest(formula = count ~ ., data = trainImp, mtry = 10,      importance = TRUE, ntree = 50) 
               Type of random forest: regression
                     Number of trees: 50
No. of variables tried at each split: 5

          Mean of squared residuals: 4280
                    % Var explained: 87.26

Comparación predicted vs ground truth.

mseDF2 <- data.frame(pred = rf2$predicted, gt = rf2$y)
mseDF2

Calculamos error

mean((mseDF2$pred-mseDF2$gt)^2) #mse
[1] 4280

Fucniona peor cogiendo solo las variables más importantes (mse 4280 > mse 2346). Nos quedaremos con el modelo ‘RF’. Al tener peor performance sobre el train, no se haran las pruebas sobre el dataset de test ya que se asume que el modelo no rendira mejor que en el caso anterior.

5. Test

Realizaremos una predicción sobre el dataset de test con el mejor modelo, en nuestro caso el RF para la entrega final.

5.1 Realizar las mismas transformaciones en el test que en el train

p1_test <- p1_test %>% mutate(workingday = as.factor(workingday),
                                   holiday = as.factor(holiday),
                                   weather = as.factor(weather),
                                   season = as.factor(season)) %>% mutate(across(where(is.numeric), scale))

5.2 Realizar la prediccion

Se realiza la prediccion sobre los datos de test con el modelo RandomForest.

predsFin <- predict(rf, p1_test)
predsFinDF <- data.frame(id = p1_test_fin$id, prediction_test = predsFin)
predsFinDF

Se genera el DF para visualizar predicciones.

5.3 Exportar resultados

Se exportan los resultados con el id y el resultado siguiendo las instrucciones del enunciado.

write.table(x = predsFinDF, file = 'p1.txt', sep = ',', row.names = FALSE, col.names = TRUE)

3. Conclusiones

El algoritmo de regresion random forest es el que mejor predice el numero de bicicletas que se alquilaran un dia, con un mse de 2000 sobre el conjunto de train. Podemos ver analizando los resultados que el modelo no tiene mucho overfitting y generaliza bien, esto es asi en parte al haber creado un arbol no demasiado grande, evitando así la caída de valores en hojas.

Teniendo en cuenta que el mse entre la media y todos los valores del dataset era de 20.000 y con el mejor modelo hemos obtenido un mse de 2000, podemos concluir que nuestro modelo ha aprendido satisfactoriamente.

El rmsle definitivo es 0.3695941.

Cómo algoritmos de regresion se podrian intentar también el xgboost o el catboost pero perderiamos entendimiento de lo que esta haciendo el modelo, asi que se ha obviado para este trabajo.

LS0tDQp0aXRsZTogIk1CRCBFc3RhZMOtc3RpY2EgLSBQcmFjdGljYSAxIg0KYXV0aG9yOiAiU2ViYXN0aWFuIEN1ZXZhLCBQb2wgR3JhY2lhIg0KZGF0ZTogIjIwLzExLzIwMjIiDQpvdXRwdXQ6DQogIGh0bWxfbm90ZWJvb2s6IGRlZmF1bHQNCiAgaHRtbF9kb2N1bWVudDogZGVmYXVsdA0KLS0tDQoNCkEgY29udGludWFjacOzbiBzZSBleHBvbmUgZWwgY8OzZGlnbyB5IGNvbWVudGFyaW9zIHJlc3VsdGFkb3MgZGUgbGEgcmVhbGl6YWNpw7NuIGRlIGxhIHByw6FjdGljYSAxLg0KDQojIDAuIEltcG9ydHMNCg0KIyMjIyBJbXBvcnRhciBsaWJyZXJpYXMNCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCmxpYnJhcnkodGlkeXZlcnNlKQ0KbGlicmFyeSh0aWR5cikNCmxpYnJhcnkoZHBseXIpDQpsaWJyYXJ5KGtuaXRyKQ0KbGlicmFyeShnZ3Bsb3QyKQ0KbGlicmFyeShzdHJpbmdyKQ0KbGlicmFyeShyZXNoYXBlKQ0KbGlicmFyeShtaW5lcnZhKQ0KbGlicmFyeShoZWF0bWFwbHkpDQpsaWJyYXJ5KGthYmxlRXh0cmEpDQpsaWJyYXJ5KGZhY3RvZXh0cmEpDQpsaWJyYXJ5KGdnYmlwbG90KQ0KbGlicmFyeShyYW5kb21Gb3Jlc3QpDQpsaWJyYXJ5KFJPQ1IpDQpsaWJyYXJ5KGdnY29ycnBsb3QpDQpsaWJyYXJ5KE1vZGVsTWV0cmljcykNCmBgYA0KDQojIyMjIEltcG9ydGFyIGRhdG9zDQoNCkF1bnF1ZSBzZSBub3MgZW50cmVnYSBlbCBmaWNoZXJvIGRlIHRyYWluIGVudGVybyB5IHBvZHJpYW1vcyBvYnRlbmVyIGxhIHZhcmlhYmxlIHJlc3B1ZXN0YSwgc2UgdHJhdGFyYW4gY8OzbW8gc2kgZGUgdW4gY2FzbyByZWFsIHNlIHRyYXRhcmEsIHkgdG9kbyBlbCBFREEgeSBlbCB0cmFpbmluZyBzZSB1c2FyYSBzb2xvIGVsIHAxX3RyYWluLiBFbCBwMV90ZXN0IHNlIHVzYXJhIMO6bmljYW1lbnRlIHBhcmEgdmFsaWRhciBsb3MgcmVzdWx0YWRvcyBhbCBmaW5hbCBkZSB0b2RvIGRlIGxhIHByYWN0aWNhLg0KDQpgYGB7cn0NCnRyYWluIDwtIHJlYWQuY3N2KCIuL2RhdGEvdHJhaW4uY3N2IiwgaGVhZCA9IFRSVUUsIHN0cmluZ3NBc0ZhY3RvcnM9VFJVRSkNCg0KcDFfdHJhaW4gPC0gcmVhZC5jc3YoIi4vZGF0YS9wMV90cmFpbi5jc3YiLCBoZWFkID0gVFJVRSwgc2VwPSI7Iiwgc3RyaW5nc0FzRmFjdG9ycz1UUlVFKQ0KDQpwMV90ZXN0IDwtIHJlYWQuY3N2KCIuL2RhdGEvcDFfdGVzdC5jc3YiLCBzZXA9IjsiLCBoZWFkID0gVFJVRSwgc3RyaW5nc0FzRmFjdG9ycz1UUlVFKQ0KcDFfdGVzdF9maW4gPC0gcmVhZC5jc3YoIi4vZGF0YS9wMV90ZXN0LmNzdiIsIHNlcD0iOyIsIGhlYWQgPSBUUlVFLCBzdHJpbmdzQXNGYWN0b3JzPVRSVUUpDQoNCmBgYA0KDQpFbCBkZiBwMV90ZXN0X2ZpbiBzZSBjcmVhIHBhcmEgbWFudGVuZXIgbG9zIGRhdG9zIGJhc2Ugc2luIHRyYW5zZm9ybWFjaW9uZXMgcGFyYSBsYSBnZW5lcmFjacOzbiBmaW5hbCBkZWwgcmVzdWx0YWRvLg0KDQojIDEuIFByZXByb2Nlc2FkbyBkZSBsb3MgZGF0b3MNCg0KRW4gZXN0ZSBhcGFydGFkbyBzZSB2YSBoYWNlciBlbCBhbsOhbGlzaSBleHBsb3JhdG9yaW8gZGUgbG9zIGRhdG9zLg0KDQpgYGB7cn0NCmhlYWQocDFfdHJhaW4pDQpgYGANCkVsaW1pbmFtb3MgbGEgdmFyaWFibGUgaWQgZGVsIGRhdGFzZXQgeWEgcXVlIGVzIGVsIGluZGljZSwgeSBubyBhcG9ydGEgaW5mb3JtYWNpw7NuLg0KDQpgYGB7cn0NCnAxX3RyYWluIDwtIHAxX3RyYWluICU+JSBzZWxlY3QoLWlkKQ0KYGBgDQoNClN1bW1hcnkgZGUgbG9zIGRhdG9zDQpgYGB7cn0NCnN1bW1hcnkocDFfdHJhaW4pDQpgYGANCg0KDQojIyMjIDEuMCBOYW4gaGFuZGxpbmcNCg0KU2UgcHVlZGUgYXByZWNpYXIgcXVlIG5vIGVuY29udHJhbW9zIG5pbmfDum4gbmFuIGVuIGxhcyBjb2x1bW5hcyBkZWwgZGF0YXNldCwgaGFjaWVuZG8gcXVlIG5vIHRlbmdhbW9zIHF1ZSB0cmF0YXIgY29uIGVsbG9zLg0KDQpgYGB7cn0NCnAxX3RyYWluICU+JSBncm91cF9ieSgpICU+JSBzdW1tYXJpc2VfYWxsKGZ1bnMoc3VtKGlzLm5hKC4pKSkpDQpgYGANCg0KDQojIyMjIDEuMSBEdXBsaWNhdGVzIGhhbmRsaW5nDQoNClNlIGVsaW1pbmFuIGxhcyBmaWxhcyBkdXBsaWNhZGFzIGRlbCBkYXRhc2V0Lg0KDQpgYGB7cn0NCnAxX3RyYWluIDwtIHAxX3RyYWluICU+JQ0KICAgICAgICAgICAgICAgIGRpc3RpbmN0KCkNCnAxX3RyYWluDQpgYGANCg0KIyMjIyAxLjIuIFNlcGFyYXIgZGF0b3MgZW50cmUgbnVtZXJpY29zIHkgY2F0ZWdvcmljb3MNCg0KUGFyYSBsYXMgdmFyaWFibGVzIHF1ZSBzb24gY2F0ZWdvcmljYXMsIGF1bnF1ZSBzdSByZXByZXNlbnRhY2nDs24gc2VhIG51bWVyaWNhIGRlYmVtb3MgdHJhdGFybGFzIGPDs21vIGNhdGVnb3JpY2FzIGEgbml2ZWwgZGF0byBwYXJhIG9idGVuZXIgZWwgcmVzdWx0YWRvIGRlc2VhZG8uDQoNCkxhcyB2YXJpYWJsZXMgY2F0ZWdvcmljYXMgZGVsIGRhdGFzZXQgc2VndW4gZWwgZW51bmNpYWRvIHNvbjogd29ya2luZ2RheSwgaG9saWRheSwgd2VhdGhlciB5IHNlYXNvbi4NCg0KYGBge3J9DQpwMV90cmFpbiA8LSBwMV90cmFpbiAlPiUgbXV0YXRlKHdvcmtpbmdkYXkgPSBhcy5mYWN0b3Iod29ya2luZ2RheSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhvbGlkYXkgPSBhcy5mYWN0b3IoaG9saWRheSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdlYXRoZXIgPSBhcy5mYWN0b3Iod2VhdGhlciksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlYXNvbiA9IGFzLmZhY3RvcihzZWFzb24pKQ0KYGBgDQoNCiMjIDIuIEV4cGxvcmF0b3J5IERhdGEgQW5hbHlzaXMNCg0KDQpFbGltaW5hbW9zIGxhIHZhcmlhYmxlIHRhcmdldCBkZWwgZGF0YXNldCBwYXJhIGhhY2VyIHVuIGVzdHVkaW8gZGUgbGFzIGNvbHVtbmFzIHByZWRpY3RpdmFzLiBQb3N0ZXJpb3JtZW50ZSBzZSBoYXLDoSBlbCBlc3R1ZGlvIGRlIGxhIHZhcmlhYmxlIHRhcmdldCBpbmRlcGVuZGllbnRlbWVudGUuDQoNCmBgYHtyfQ0KcHJlZGljdG9ycyA8LSBwMV90cmFpbiAlPiUgc2VsZWN0KC1jb3VudCkNCmBgYA0KDQoNCiMjIyMgMi4wLiBIYWNlciBib3hwbG90cyBkZSBsb3MgY2FtcG9zIHkgbWlyYXIgb3V0bGllcnMgZW4gbG9zIGRhdG9zDQoNCkVzdHVkaW8gZGUgbGFzIHZhcmlhYmxlcyBwcmVkaWN0aXZhcyBudW1lcmljYXMuIA0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KbWVsdFByZWQgPC0gcHJlZGljdG9ycyAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSAlPiUgbWVsdCgpDQptZWx0UHJlZCAlPiUNCiBnZ3Bsb3QoYWVzKGZhY3Rvcih2YXJpYWJsZSksIHZhbHVlKSkgKw0KICAgZ2VvbV92aW9saW4od2lkdGg9MSwgY29sb3IgPSAiZ3JheSIsIGFscGhhID0gMC4yLCBmaWxsID0gJ2dyZWVuJyApICsNCiAgICBnZW9tX2JveHBsb3Qod2lkdGg9MC4zLCBjb2xvcj0iYmxhY2siLCBmaWxsPSdibHVlJywgYWxwaGE9MC4zLCBvdXRsaWVyLmNvbG91cj0icmVkIiwgb3V0bGllci5zaGFwZT04LA0KICAgICAgICAgICAgIG91dGxpZXIuc2l6ZT0xLCBub3RjaD1UUlVFKSArIGZhY2V0X3dyYXAofnZhcmlhYmxlLCBzY2FsZT0iZnJlZSIpDQpgYGANCkVzdG9zIGdyw6FmaWNvcyByZXByZXNlbnRhbiB1biBib3hwbG90IHkgdW4gdmlvbGlucGxvdCBzb2JyZSB0b2RhcyBsYXMgdmFyaWFibGVzIG51bWVyaWNhcyBkZWwgZGF0YXNldC4gUG9kZW1vcyBhcHJlY2lhcjoNCg0KLSBMYSBjb2x1bW5hIHllYXIgY29udGllbmUgw7puaWNhbWVudGUgZG9zIHZhbG9yZXMgMjAxMSB5IDIwMTIuDQoNCi0gTGEgY29sdW1uYSBob3VyIHNlIGRpdHJpYnV5ZSBlbnRyZSBlbCAwIHkgZWwgMjQsIGNvbiB1bmEgbWVkaWEgc2l0dWFkYSBlbiBsb3MgMTIuIE5vIGVuY29udHJhbW9zIG91dGxpZXJzLg0KDQotIExhIGNvbHVtbmEgdGVtcCB5IGxhIGNvbHVtbmEgYXRlbXAgc2lndWVuIGRpc3RyaWJ1Y2lvbmVzIG11eSBzaW1pbGFyZXMsIGxhIG1lZGlhIGVzdGEgYWxyZWRlZG9yIGRlbCAyNS4gTm8gZW5jb250cmFtb3Mgb3V0bGllcnMuDQoNCi0gTGEgY29sdW1uYSBodW1pZGl0eSBzZSBkaXN0cmlidXllIGVudHJlIGVsIDAgeSBlbCAxMDAgY29uIHVuYSBtZWRpYSBhbHJlZGVkb3IgZGUgbG9zIDYwLiBObyBlbmNvbnRyYW1vcyBvdHVsaWVycy4NCg0KLSBMYSBjb2x1bW5hIHdpbmRzcGVlZCBzZSBkaXN0cmlidXllIGVudHJlIGVsIDAgeSBlbCA2MCBjb24gdW5hIG1lZGlhIGFscmVkZWRvciBkZSBsb3MgMTUuIEVuY29udHJhbW9zIG51bWVyc29zIG91dGxpZXJzIGN1YW5kbyBlbCB2YWxvcmVzIGVzIG1heW9yIGRlIDMwLg0KDQoNCiMjIyMgMi4xIE1hdHJpeiBkZSBjb3JyZWxhY2lvbmVzIGRlIHBlYXJzb24gZGUgbGFzIHZhcmlhYmxlcyBudW1lcmljYXMNCmBgYHtyfQ0KY29ycnAgPC0gcDFfdHJhaW4gJT4lIHNlbGVjdCh3aGVyZShpcy5udW1lcmljKSkgJT4lICBjb3IobWV0aG9kID0gJ3BlYXJzb24nKQ0KY29ycnAgJT4lIGdnY29ycnBsb3QoaGMub3JkZXIgPSBUUlVFLCB0eXBlID0gJ2xvd2VyJywgbGFiPVRSVUUsDQogICAgICAgICAgICAgICAgICAgICAgb3V0bGluZS5jb2wgPSAid2hpdGUiLA0KICAgICAgICAgICAgICAgICAgICAgICBnZ3RoZW1lID0gZ2dwbG90Mjo6dGhlbWVfZ3JheSwNCiAgICAgICAgICAgICAgICAgICAgICAgY29sb3JzID0gYygiIzZEOUVDMSIsICJ3aGl0ZSIsICIjRTQ2NzI2IikNCiAgICAgICAgICAgICAgICAgICAgICApDQoNCmBgYA0KU2UgcHVlZGUgYXByZWNpYXIgY8OzbW8gbGEgdmFyaWFibGUgYXRlbXAgeSB0ZW1wIGVzdGFuIG11eSBjb3JyZWxhY2lvbmFkYXMsIGRlIGFtYmFzLCBzZWxlY2Npb25hcmVtb3Mgc29sYW1lbnRlIGxhIGNvbHVtbmEgdGVtcCB5IGVsaW1pbmFyZW1vcyBsYSBjb2x1bW5hIGF0ZW1wIHBhcmEgdW5hIG1lam9yYSBlbiBsYSBwcmVkaWNjaW9uLg0KQ29uIGxhIHZhcmlhYmxlIHRhcmdldCBjb3VudCwgZXN0YW4gZGlyZWN0YW1lbnRlIHJlbGFjaW9uYWRhcyBsYXMgY29sdW1uYXMgdGVtcCwgeWVhciwgaG91ciB5IHdpbmRzcGVlZCB5IGxhIGNvbHVtbmEgaHVtaWRpdHkgZXN0YSBpbmRpcmVjdGFtZW50ZSByZWFsY2lvbmFkYS4NCg0KDQpFbGltaW5hbW9zIGxhIGNvbHVtbmEgYXRlbXAgZGVsIGRhdGFzZXQuDQpgYGB7cn0NCnAxX3RyYWluIDwtIHAxX3RyYWluICU+JSBzZWxlY3QoLWF0ZW1wKQ0KcDFfdGVzdCA8LSBwMV90ZXN0ICU+JSBzZWxlY3QoLWF0ZW1wKQ0KcHJlZGljdG9ycyA8LSBwcmVkaWN0b3JzICU+JSBzZWxlY3QoLWF0ZW1wKQ0KYGBgDQoNCiMjIyMgMi4yIG1hdHJpeiBkZSBjb3JyZWxhY2lvbmVzIGxpbmVhbGVzIHkgbm8gbGluZWFsZXMgY29uIGxhIHZhcmlhYmxlIHRhcmdldCAnY291bnQnDQoNCkNhbGN1bGFtb3MgbGEgY29ycmVsYWNpw7NuIG5vIGxpbmVhbCBNSUMgZGUgbGFzIGNvbHVtbmFzIGNvbiBsYSB2YXJpYWJsZSB0YXJnZXQuDQpgYGB7cn0NCm51bXByZWQgPC0gcHJlZGljdG9ycyAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSANCmNvcnJzIDwtIGRhdGEuZnJhbWUoTUlDID0gbWluZShudW1wcmVkLCB5ID0gcDFfdHJhaW4kY291bnQsIGFscGhhID0gMC43KSkgJT4lIHNlbGVjdChNSUMuWSkNCmNvcnJzJFBlYXJzb24gPC0gY29yKG51bXByZWQsIHkgPSBwMV90cmFpbiRjb3VudCwgbWV0aG9kID0gJ3BlYXJzb24nKQ0Kcm93bmFtZXMoY29ycnMpIDwtIHJvd25hbWVzKGNvcnJzJFBlYXJzb24pDQpjb3Jycw0KYGBgDQpQb2RyZW1vcyBhcHJlY2lhciBxdWUgbGFzIGNvcnJlbGFjaW9uZXMgbm8gbGluZWFsYXMgY2FsY3VsYWRhcyBzb24gKGVuIHByb3BvcmNpb24pIHNpbWlsYXJlcyBhIGxhcyBjb3JyZWxhY2lvbmVzIGRlIFBlYXJzb24sIGVudGVuZGllbmRvIGVudG9uY2VzIHF1ZSBubyBoYXkgY29ycmVsYWNpb25lcyBvYnZpYWRhcyBlbiBlbCBncmFmaWNvIGRlIGNvcnJlbGFjaW9uZXMuIA0KDQoNCmBgYHtyfQ0KY2JpbmQoY29ycnMkTUlDLlksIGNvcnJzJE1JQy5ZICkgJT4lIGhlYXRtYXBseV9jb3IoeGxhYiA9ICIiLA0KICAgICAgICAgICAgICB5bGFiID0gIkNvbHVtbmFzIiwgbWFpbiA9ICJDb3JyZWxhY2lvbiBNSUMgZGUgbGFzIGNvbHVtbmFzIG51bWVyaWNhcyBjb24gbGEgdmFyaWFibGUgdGFyZ2V0ICdjb3VudCciLCBjZWxsbm90ZSA9IGNiaW5kKGNvcnJzJE1JQy5ZLCBjb3JycyRNSUMuWSApLCBrX2NvbCA9IDEsIGtfcm93ID0gMSkNCg0KYGBgDQpFbCBncsOgZmljbyBzdXBlcmlvciB0aWVuZSAyIGNvbHVtbmFzIHBlcm8gc2UgZGViZSBjb25zaWRlcmFyIHVuYSBzb2xhIChlc3RhbiByZXBldGlkYXMpLCBoYSBzaWRvIGxhIHVuaWNhIG1hbmVyYSBxdWUgaGVtb3MgZW5jb250cmFkbyBkZSBoYWNlciBlbCBncsOhZmljbyBkZWwgTUlDLg0KDQoNCiMjIyMgMi4zIFBDQSANCg0KUmVhbGl6YW1vcyB1biBQQ0Egc29icmUgbG9zIGRhdG9zIHBhcmEgZXh0cmFlciBsb3MgY29tcG9uZW50ZXMgcHJpbmNpcGFsZXMgeSBlc3R1ZGlhcmxvcy4NCg0KYGBge3J9DQpwY2EgPC0gcHJlZGljdG9ycyAlPiUgc2VsZWN0KHdoZXJlKGlzLm51bWVyaWMpKSAlPiUgc2FtcGxlX24oMjAwKSAlPiUNCiAgcHJjb21wKHNjYWxlLiA9IFRSVUUsIGNlbnRlciA9IFRSVUUpICNTZSBkZWJlIGhhY2VyIHVuIHNhbXBsZSBwYXJhIHBvc3Rlcmlvcm1lbnRlIHBvZGVyIGFwcmVjaWFyIGdyYWZpY2FtZW50ZSBsYSBkaXN0cmlidWNpw7NuIFBDQS4NCnN1bW1hcnkocGNhKQ0KYGBgDQoNCmBgYHtyfQ0KcGNhICU+JSANCiAgZ2diaXBsb3Q6OmdnYmlwbG90KHNjYWxlID0gMSkNCmBgYA0KDQpHcsOgZmljbyBxdWUgcmVwcmVzZW50YSBsYSBwb25kZXJhY2nDs24gZGUgbGFzIHZhcmlhYmxlcyB1dGlsaXphZGFzIHBhcmEgZWwgUENBLg0KDQoNCiMjIDMuIEFuYWxpc2lzIGV4cGxvcmF0b3JpbyBkZSBsYSB2YXJpYWJsZSB0YXJnZXQgJ2NvdW50JyANCg0KDQpMYSB2YXJpYWJsZSB0YXJnZXQgJ2NvdW50JyBlcyB1bmEgdmFyaWFibGUgbnVtZXJpY2EgcXVlIHJlcHJlc2VudGEgZWwgbnVtZXJvIGRlIGJpY2ljbGV0YXMgYWxxdWlsYWRhcyBlbiBsYXMgZ3JhbmRlcyBjaXVkYWRlcy4gTGEgdmFyaWFibGUgdGllbmUgdW5hIG1lZGlhIGRlIDE5MCB5IHVuIHN0ZCBkZSAxODIsIHVuIHZhbG9yIGJhc3RhbnRlIGVsZXZhZG8gcXVlIGluZGljYSBtdWNoYSB2YXJpYWNpw7NuIGVuIGxhIHZhcmlhYmxlLg0KDQpgYGB7cn0NCmNvcnJzJFBlYXJzb24NCmBgYA0KQ29ycmVsYWNpb25lcyBkZSBwZWFyc29uIGRlIGxhcyBjb2x1bW5hcyBwcmVkaWN0aXZhcyBjb24gbGEgdmFyaWFibGUgJ2NvdW50Jy4NCg0KYGBge3J9DQptZWFuKHAxX3RyYWluJGNvdW50KQ0Kc2QocDFfdHJhaW4kY291bnQpDQpgYGANCg0KIyMjIyAzLjEgRGlzdHJpYnVjacOzbg0KDQpFbCBncsOhZmljbyBzaWd1aWVudGUgZXMgdW4gYm94cGxvdCB5IHVuIHZpb2xpbiBwbG90IHNvYnJlIGxhIHZhcmlhYmxlIHRhcmdldCAnY291bnQnLiBQb3IgZWwgdmlvbGluIHBsb3Qgc2UgYXByZWNpYSBxdWUgbGEgbWF5b3JpYSBkZSB2YWxvcmVzIHNlIGVuY3VlbnRyYW4gY2VyY2Fub3MgYWwgMCBwZXJvIHBvciBlbCBib3hwbG90IHNlIHZpc3VhbGl6YSBxdWUgbG9zIHZhbG9yZXMgYWx0b3MgZGVsIGRhdGFzZXQgaGFjZW4gcXVlIGxhIG1lZGlhIHN1YmEgY29uc2lkZXJhYmxlbWVudGUuIEVuIGVsIGJveHBsb3QgcG9kZW1vcyBhcHJlY2lhciB0YW1iacOpbiBkaXZlcnNvcyBvdXRsaWVycyBxdWUgc29icmVzYWxlbiBhIHBhcnRpciBkZWwgdmFsb3IgNTUwLg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KcDFfdHJhaW4gJT4lDQogIGdncGxvdCggYWVzKHg9Y291bnQsIHk9Y291bnQpKSArDQogICAgZ2VvbV92aW9saW4od2lkdGg9MSwgY29sb3IgPSAiZ3JheSIsIGFscGhhID0gMC4yLCBmaWxsID0gJ2dyZWVuJyApICsNCiAgICBnZW9tX2JveHBsb3Qod2lkdGg9MC4zLCBjb2xvcj0iYmxhY2siLCBmaWxsPSdibHVlJywgYWxwaGE9MC4zLCBvdXRsaWVyLmNvbG91cj0icmVkIiwgb3V0bGllci5zaGFwZT04LA0KICAgICAgICAgICAgIG91dGxpZXIuc2l6ZT0xLCBub3RjaD1UUlVFKSArDQogICAgdGhlbWUoDQogICAgICBsZWdlbmQucG9zaXRpb249Im5vbmUiLA0KICAgICAgcGxvdC50aXRsZSA9IGVsZW1lbnRfdGV4dChzaXplPTExKQ0KICAgICkgKw0KICAgIGdndGl0bGUoIlZpb2xpbiB5IGJveCBwbG90IGRlIGxvcyB2YWxvcmVzIGRlIGxhIHZhcmlhYmxlIHRhcmdldCAiKSArDQogICAgeGxhYigiIikgKyB0aGVtZV9jbGFzc2ljKCkgDQoNCmBgYA0KDQoNCiMjIyMgMy4yIEhpc3RvZ3JhbWEgeSBkZW5zaWRhZA0KDQpQYXJ0aWVuZG8gZGUgbGEgYmFzZSBxdWUgbGEgdmFyaWFibGUgdGFyZ2V0IGVzIGNvbnRpbnVhLCBwb2RlbW9zIGFwcmVjaWFyIHF1ZSBsYSBmcmVxdWVuY2lhIGRlIGxhIG1pc21hIHNpZ3VlIHVuYSBkaXN0cmlidWNpw7NuIHBhcmVjaWRhIGEgbGEgZXhwb25lbmNpYWwsIHlhIHF1ZSBjw7NtbyBtw6FzIHNlIGFsZWphIGRlbCBuw7ptZXJvIDAgbWVub3MgZnJlcXVlbmNpYSBlbmNvbnRyYW1vcy4NCkhhYnLDoSBxdWUgdGVuZXIgZW4gY3VlbnRhIGVudG9uY2VzLCBxdWUgZWwgbW9kZWxvIHZhIGEgdGVuZGVyIGEgcHJlZGVjaXIgdmFsb3JlcyBiYWpvcyBlbiBsYSBtYXlvcmlhIGRlIGxvcyBjYXNvcy4NCg0KYGBge3J9DQpwMV90cmFpbiAlPiUNCmdncGxvdCggYWVzKHg9Y291bnQpKSArIA0KIGdlb21faGlzdG9ncmFtKGFlcyh5PS4uZGVuc2l0eS4uKSwgY29sb3VyPSJibGFjayIsIGZpbGw9IndoaXRlIiwgYmlud2lkdGggPSAyMCkrDQogZ2VvbV9kZW5zaXR5KGFscGhhPS4yLCBmaWxsPSIjRkY2NjY2IikgKyB0aGVtZV9jbGFzc2ljKCkNCg0KYGBgDQoNCg0KIyMgNC4gUHJlZGljY2lvbg0KDQojIyMjIDQuMSBOb3JtYWxpemFjacOzbiB5IHByZXBhcmFjacOzbiBkZSBsb3MgZGF0b3MNCg0KSGFjZW1vcyB1biBlc2NhbGFkbyBzb2JyZSBsb3MgZGF0b3MgcGFyYSBub3JtYWxpemFyLiANCg0KYGBge3J9DQpwMV90cmFpbiA8LSBwMV90cmFpbiAlPiUgbXV0YXRlKGFjcm9zcyh3aGVyZShpcy5udW1lcmljKSwgc2NhbGUpKQ0KYGBgDQoNClNlcGFyYW1vcyBlbCBkYXRhc2V0IHBhcmEgdHJhaW4geSB0ZXN0LCBwYXJhIHBvZGVyIGhhY2VyIHZhbGlkYWNpb25lcyBzb2JyZSBlbCBtb2RlbG8uDQoNCmBgYHtyfQ0KcCA8LSAwLjgNCm4gPC0gbnJvdyhwMV90cmFpbikNCnNldC5zZWVkKDEyMzQ1KQ0KdHJhaW4uc2VsIDwtIHNhbXBsZShjKEZBTFNFLCBUUlVFKSwgbiwgcmVwID0gVFJVRSwgcHJvYj0oYygxLXAscCkpKQ0KdHJhaW4gPC0gcDFfdHJhaW5bdHJhaW4uc2VsLF0NCnRlc3QgPC0gcDFfdHJhaW5bIXRyYWluLnNlbCxdDQoNCnRyYWluDQpgYGANCg0KDQojIyMjIDQuMiBMaW5lYXIgbW9kZWwNCg0KRW50cmVuYW1vcyB1biBtb2RlbG8gbGluZWFsIGNvbiBsb3MgZGF0b3MgZGVsIHRyYWluDQpgYGB7cn0NCm1vZF9sbSA8LSBsbShjb3VudH4uLCB0cmFpbikgDQpgYGANCg0KQ2FsY3VsYW1vcyBlbCBtc2Ugc29icmUgZWwgdHJhaW4NCmBgYHtyfQ0KbWVhbihtb2RfbG0kcmVzaWR1YWxzXjIpICNtc2UNCmBgYA0KQ2FsY3VsYW1vcyBlbCBtc2xlIHNvYnJlIGVsIHRyYWluDQpgYGB7cn0NCm1lYW4obG9nKG1vZF9sbSRyZXNpZHVhbHNeMikpICNtc2UNCmBgYA0KDQpTZSBwdWVkZSBhcHJlY2lhciBxdWUgZWwgbXNlIHkgbXNsZSBlcyBtdXkgZ3JhbmRlLCBlbCBtb2RlbG8gbm8gZnVuY2lvbmEgYmllbi4NCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCnBsb3QocDFfdHJhaW4kY291bnQpDQphYmxpbmUobW9kX2xtKQ0KYGBgDQoNClNlIHB1ZWRlIHZlciBjw7NtbyB1biBtb2RlbG8gbGluZWFsIGVzIGRlbWFzaWFkbyBzZW5jaWxsbyBwYXJhIGVudGVuZGVyIGxhIGNvbXBsZWppZGFkIGRlIGxvcyBkYXRvcyB5IHNlbnppbGxhbWVudGUgcHJlZGljZSBsYSBtZWRpYSBkZWwgcmVzdWx0YWRvLiANCg0KUG9kcmVtb3MgY29nZXIgZWwgZXJyb3IgMjAwMDAgKHF1ZSByZXByZXNlbnRhIGVsIG1zZSBlbnRyZSBsYSBtZWRpYSB5IGNhZGEgdmFsb3IpIHBhcmEgcG9kZXIgZGVjaWRpciBlbiBsb3MgbW9kZWxvcyBwb3N0ZXJpb3JlcyBzaSBlc3RhbiBhcHJlbmRpZW5kby4NCg0KVmFsaWRhY2nDs24gc29icmUgZWwgdGVzdA0KYGBge3J9DQpwcmVkc190ZXN0X2xtIDwtIHByZWRpY3QobW9kX2xtLCB0ZXN0KQ0KcHJlZHNMTSA8LSBkYXRhLmZyYW1lKHByZWQgPSBwcmVkc190ZXN0X2xtLCBndCA9IHRlc3QkY291bnQpDQpwcmVkc0xNDQpgYGANCg0KU2UgcHVlZGUgYXByZWNpYXIgY8OzbW8gZXJhIGRlIGVzcGVyYXIsIHF1ZSBsYSBwZXJmb3JtYW5jZSBkZWwgbW9kZWxvIHNvYnJlIGVsIGRhdGFzZXQgZGUgdmFsaWRhY2nDs24gZXMgbWFsYS4NCg0KQ2FsY3VsYW1vcyBlbCBNU0Ugc29icmUgZWwgdGVzdA0KDQpgYGB7cn0NCm1lYW4oKHByZWRzTE0kcHJlZC1wcmVkc0xNJGd0KV4yKSAjbXNlDQpgYGANCkNhbGN1bGFtb3MgUk1TTEUgKGVycm9yIGN1YWRyYXRpY28gbWVkaW8gbG9nYXJpdG1pY28pIHNvYnJlIGVsIHRlc3QNCmBgYHtyfQ0Kcm1zbGUocHJlZHNMTSRndCxwcmVkc0xNJHByZWQpDQpgYGANCg0KIyMjIyA0LjMgZ2xtDQoNCmBgYHtyfQ0KbW9kX2dsbSA8LSBnbG0oY291bnR+LiwgdHJhaW4sIGZhbWlseSA9IHBvaXNzb24oKSkNCm1lYW4obW9kX2dsbSRyZXNpZHVhbHNeMikNCmBgYA0KDQpTZSBoYSBpbnRlbnRhZG8gYW5hbGl6YXIgbGEgcHJlZGljY2nDs24gcGVybyBubyBzZSBlbmN1ZW50cmEgc2VudGlkbyBlbiBsYSBtaXNtYS4gRWwgbXNlIGRlIDAuNzkgcGFyZWNlIGRlbWFzaWFkbyBiYWpvIHBhcmEgc2VyIGNvcnJlY3RvIHkgZWwgcHJlZGljdCBzb2JyZSBlbCB0cmFpbiBkYSB2YWxvcmVzIGNvdW50IGxlamFub3MgYWwgZ3JvdW5kIHRydXRoLg0KDQoNCiMjIyMgNC40IFJhbmRvbSBmb3Jlc3QNCg0KYGBge3J9DQpzZXQuc2VlZCgxMjM0KSAjZGVmaW5pbW9zIHVuYSByYW5kb20gc2VlZCBwYXJhIG9idGVuZXIgbG9zIG1pc21vcyByZXN1bHRhZG9zIGNvbnNpc3RlbnRlbWVudGUNCmBgYA0KDQpEZWZpbmltb3MgZWwgbW9kZWxvLiBEdXJhbnRlIGxhIHJlYWxpemFjacOzbiBkZSBsYSBwcsOhY3RpY2Egc2UgaGEgaGVjaG8gdW4gZ3JpZCBzZWFyY2ggeSBzZSBkZWZpbmVuIGxvcyBwYXJhbWV0cm9zIHF1ZSBkZXZ1ZWx2ZW4gbWVqb3IgcmVzdWx0YWRvLg0KDQpgYGB7ciB3YXJuaW5nPUZBTFNFfQ0KcmYgPC0gcmFuZG9tRm9yZXN0KGNvdW50fi4sIGRhdGEgPSB0cmFpbiwgbXRyeSA9IDEwLCBpbXBvcnRhbmNlID0gVFJVRSwgbnRyZWUgPSA1MCApDQpyZg0KYGBgDQoNClZpc3VhbGl6YW1vcyBxdWUgcHJlZGljdG9yZXMgc29uIG3DoXMgaW1wb3J0YW50ZXMuDQpgYGB7cn0NCmltcG9ydGFuY2UocmYpDQp2YXJJbXBQbG90KHJmKQ0KYGBgDQpQb2RlbW9zIGFwcmVjaWFyIGPDs21vIGxhIHZhcmlhYmxlICdob3VyJyBlcyBjb24gZGlmZXJlbmNpYSBsYSBxdWUgbWVqb3IgYXl1ZGEgYSBwcmVkZWNpciBlbCBudW1lcm8gZGUgYmljaWNsZXRhcyBhbHF1aWxhZGFzLg0KDQoNCkFuYWxpc2lzIGRlbCBtb2RlbCBwZXJmb3JtYW5jZSAobXNlKSBzb2JyZSBlbCBjb25qdW50byBkZSB0cmFpbg0KDQpgYGB7cn0NCm1zZURGIDwtIGRhdGEuZnJhbWUocHJlZCA9IHJmJHByZWRpY3RlZCwgZ3QgPSByZiR5KQ0KbXNlREYNCmBgYA0KUG9kZW1vcyB2ZXIgbGFzIGRpZmVyZW5jaWFzIGVudHJlIGVsIGNvdW50IHByZWRlY2lkbyB5IGVsIGdyb3VuZCB0cnV0aC4NCg0KYGBge3J9DQptZWFuKChtc2VERiRwcmVkLW1zZURGJGd0KV4yKSAjbXNlDQpgYGANCkNhbGN1bGFtb3MgUk1TTEUgKGVycm9yIGN1YWRyYXRpY28gbWVkaW8gbG9nYXJpdG1pY28pDQpgYGB7cn0NCnJtc2xlKG1zZURGJGd0LG1zZURGJHByZWQpDQpgYGANCg0KT2J0ZW5lbW9zIHVuIG1zZSB5IHJtc2xlIGNvaGVyZW50ZXMsIGVsIG1vZGVsbyBmdW5jaW9uYS4NCg0KVmFsaWRhY2lvbiBkZWwgbW9kZWxvIHNvYnJlIGVsIHRlc3QNCmBgYHtyfQ0KcHJlZF90ZXN0X3JmIDwtIHByZWRpY3QocmYsIHRlc3QpDQptc2VERnRlc3QgPC0gZGF0YS5mcmFtZShwcmVkID0gcHJlZF90ZXN0X3JmLCBndCA9IHRlc3QkY291bnQpDQptc2VERnRlc3QNCmBgYA0KUG9kZW1vcyB2ZXIgbGFzIGRpZmVyZW5jaWFzIGVudHJlIGVsIGNvdW50IHByZWRlY2lkbyB5IGVsIGdyb3VuZCB0cnV0aC4NCg0KYGBge3J9DQptZWFuKChtc2VERnRlc3QkcHJlZC1tc2VERnRlc3QkZ3QpXjIpICNtc2UNCmBgYA0KQ2FsY3VsYW1vcyBSTVNMRSAoZXJyb3IgY3VhZHJhdGljbyBtZWRpbyBsb2dhcml0bWljbykNCmBgYHtyfQ0Kcm1zbGUobXNlREZ0ZXN0JGd0LG1zZURGdGVzdCRwcmVkKQ0KYGBgDQoNCkVsIHJlc3VsdGFkbyBkZWwgbXNlIHkgZWwgcm1zbGUgc29icmUgZWwgZGF0YXNldCBkZSB0ZXN0IGVzIG11eSBwYXJlY2lvZG8gYSBsb3MgcmVzdWx0YWRvcyBkZSB0cmFpbiwgZGUgZXN0YSBtYW5lcmEgcG9kZW1vcyBjb25jbHVpciBxdWUgbnVlc3RybyBtb2RlbG8gbm8gdGllbmUgb3ZlcmZpdHRpbmcgeSBlcyBjYXBheiBkZSBnZW5lcmFsaXphciB5IHRlbmVyIGJ1ZW4gcmVuZGltaWVudG8gcHJlZGljdGl2byBzb2JyZSBkYXRvcyBxdWUgbm8gaGEgdmlzdG8gY29uIGFudGVyaW9yaWRhZC4NCg0KIyMjIyMjIDQuNC4xIFJhbmRvbSBGb3Jlc3QgMg0KDQpWb2x2ZW1vcyBhIGhhY2VyIHJhbmRvbSBmb3Jlc3QgY29uIHZhcmlhYmxlcyBtw6FzIGNvcnJlbGFjaW9uYWRhcyBjb24gbGEgY29sdW1uYSBhIHByZWRlY2lyLg0KDQpgYGB7cn0NCnRyYWluSW1wIDwtIHRyYWluICU+JSBzZWxlY3QoJ2hvdXInLCd5ZWFyJywnd29ya2luZ2RheScsJ3NlYXNvbicsJ2h1bWlkaXR5JywgJ2NvdW50JykNCmBgYA0KDQpFbnRyZW5hbW9zIHJhbmRvbSBmb3Jlc3QNCg0KYGBge3Igd2FybmluZz1GQUxTRX0NCnJmMiA8LSByYW5kb21Gb3Jlc3QoY291bnR+LiwgZGF0YSA9IHRyYWluSW1wLCBtdHJ5ID0gMTAsIGltcG9ydGFuY2UgPSBUUlVFLCBudHJlZSA9IDUwICkNCnJmMg0KYGBgDQoNCkNvbXBhcmFjacOzbiBwcmVkaWN0ZWQgdnMgZ3JvdW5kIHRydXRoLg0KYGBge3J9DQptc2VERjIgPC0gZGF0YS5mcmFtZShwcmVkID0gcmYyJHByZWRpY3RlZCwgZ3QgPSByZjIkeSkNCm1zZURGMg0KYGBgDQoNCkNhbGN1bGFtb3MgZXJyb3INCmBgYHtyfQ0KbWVhbigobXNlREYyJHByZWQtbXNlREYyJGd0KV4yKSAjbXNlDQpgYGANCkZ1Y25pb25hIHBlb3IgY29naWVuZG8gc29sbyBsYXMgdmFyaWFibGVzIG3DoXMgaW1wb3J0YW50ZXMgKG1zZSA0MjgwID4gbXNlIDIzNDYpLiBOb3MgcXVlZGFyZW1vcyBjb24gZWwgbW9kZWxvICdSRicuDQpBbCB0ZW5lciBwZW9yIHBlcmZvcm1hbmNlIHNvYnJlIGVsIHRyYWluLCBubyBzZSBoYXJhbiBsYXMgcHJ1ZWJhcyBzb2JyZSBlbCBkYXRhc2V0IGRlIHRlc3QgeWEgcXVlIHNlIGFzdW1lIHF1ZSBlbCBtb2RlbG8gbm8gcmVuZGlyYSBtZWpvciBxdWUgZW4gZWwgY2FzbyBhbnRlcmlvci4NCg0KIyMgNS4gVGVzdA0KUmVhbGl6YXJlbW9zIHVuYSBwcmVkaWNjacOzbiBzb2JyZSBlbCBkYXRhc2V0IGRlIHRlc3QgY29uIGVsIG1lam9yIG1vZGVsbywgZW4gbnVlc3RybyBjYXNvIGVsIFJGIHBhcmEgbGEgZW50cmVnYSBmaW5hbC4NCg0KIyMjIyA1LjEgUmVhbGl6YXIgbGFzIG1pc21hcyB0cmFuc2Zvcm1hY2lvbmVzIGVuIGVsIHRlc3QgcXVlIGVuIGVsIHRyYWluDQpgYGB7cn0NCnAxX3Rlc3QgPC0gcDFfdGVzdCAlPiUgbXV0YXRlKHdvcmtpbmdkYXkgPSBhcy5mYWN0b3Iod29ya2luZ2RheSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIGhvbGlkYXkgPSBhcy5mYWN0b3IoaG9saWRheSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHdlYXRoZXIgPSBhcy5mYWN0b3Iod2VhdGhlciksDQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlYXNvbiA9IGFzLmZhY3RvcihzZWFzb24pKSAlPiUgbXV0YXRlKGFjcm9zcyh3aGVyZShpcy5udW1lcmljKSwgc2NhbGUpKQ0KYGBgDQoNCiMjIyMgNS4yIFJlYWxpemFyIGxhIHByZWRpY2Npb24NClNlIHJlYWxpemEgbGEgcHJlZGljY2lvbiBzb2JyZSBsb3MgZGF0b3MgZGUgdGVzdCBjb24gZWwgbW9kZWxvIFJhbmRvbUZvcmVzdC4NCg0KYGBge3J9DQpwcmVkc0ZpbiA8LSBwcmVkaWN0KHJmLCBwMV90ZXN0KQ0KcHJlZHNGaW5ERiA8LSBkYXRhLmZyYW1lKGlkID0gcDFfdGVzdF9maW4kaWQsIHByZWRpY3Rpb25fdGVzdCA9IHByZWRzRmluKQ0KcHJlZHNGaW5ERg0KYGBgDQpTZSBnZW5lcmEgZWwgREYgcGFyYSB2aXN1YWxpemFyIHByZWRpY2Npb25lcy4NCg0KIyMjIyA1LjMgRXhwb3J0YXIgcmVzdWx0YWRvcw0KU2UgZXhwb3J0YW4gbG9zIHJlc3VsdGFkb3MgY29uIGVsIGlkIHkgZWwgcmVzdWx0YWRvIHNpZ3VpZW5kbyBsYXMgaW5zdHJ1Y2Npb25lcyBkZWwgZW51bmNpYWRvLg0KDQpgYGB7cn0NCndyaXRlLnRhYmxlKHggPSBwcmVkc0ZpbkRGLCBmaWxlID0gJ3AxLnR4dCcsIHNlcCA9ICcsJywgcm93Lm5hbWVzID0gRkFMU0UsIGNvbC5uYW1lcyA9IFRSVUUpDQpgYGANCg0KIyMgMy4gQ29uY2x1c2lvbmVzDQoNCkVsIGFsZ29yaXRtbyBkZSByZWdyZXNpb24gcmFuZG9tIGZvcmVzdCBlcyBlbCBxdWUgbWVqb3IgcHJlZGljZSBlbCBudW1lcm8gZGUgYmljaWNsZXRhcyBxdWUgc2UgYWxxdWlsYXJhbiB1biBkaWEsIGNvbiB1biBtc2UgZGUgMjAwMCBzb2JyZSBlbCBjb25qdW50byBkZSB0cmFpbi4gUG9kZW1vcyB2ZXIgYW5hbGl6YW5kbyBsb3MgcmVzdWx0YWRvcyBxdWUgZWwgbW9kZWxvIG5vIHRpZW5lIG11Y2hvIG92ZXJmaXR0aW5nIHkgZ2VuZXJhbGl6YSBiaWVuLCBlc3RvIGVzIGFzaSBlbiBwYXJ0ZSBhbCBoYWJlciBjcmVhZG8gdW4gYXJib2wgbm8gZGVtYXNpYWRvIGdyYW5kZSwgZXZpdGFuZG8gYXPDrSBsYSBjYcOtZGEgZGUgdmFsb3JlcyBlbiBob2phcy4gDQoNClRlbmllbmRvIGVuIGN1ZW50YSBxdWUgZWwgbXNlIGVudHJlIGxhIG1lZGlhIHkgdG9kb3MgbG9zIHZhbG9yZXMgZGVsIGRhdGFzZXQgZXJhIGRlIDIwLjAwMCB5IGNvbiBlbCBtZWpvciBtb2RlbG8gaGVtb3Mgb2J0ZW5pZG8gdW4gbXNlIGRlIDIwMDAsIHBvZGVtb3MgY29uY2x1aXIgcXVlIG51ZXN0cm8gbW9kZWxvIGhhIGFwcmVuZGlkbyBzYXRpc2ZhY3RvcmlhbWVudGUuDQoNCkVsIHJtc2xlIGRlZmluaXRpdm8gZXMgMC4zNjk1OTQxLg0KDQpDw7NtbyBhbGdvcml0bW9zIGRlIHJlZ3Jlc2lvbiBzZSBwb2RyaWFuIGludGVudGFyIHRhbWJpw6luIGVsIHhnYm9vc3QgbyBlbCBjYXRib29zdCBwZXJvIHBlcmRlcmlhbW9zIGVudGVuZGltaWVudG8gZGUgbG8gcXVlIGVzdGEgaGFjaWVuZG8gZWwgbW9kZWxvLCBhc2kgcXVlIHNlIGhhIG9idmlhZG8gcGFyYSBlc3RlIHRyYWJham8uDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0KDQoNCg0K